﻿//
// (C) Copyright 2009 Irantha Suwandarathna (iranthas@hotmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EffiProz.Core.Lib;
using EffiProz.Core.DataTypes;
using EffiProz.Core.Indexes;
using EffiProz.Core.Errors;
using EffiProz.Core.Persist;
using EffiProz.Core.Results;
using EffiProz.Core.Navigators;

namespace EffiProz.Core
{
    public class QueryExpression
    {

        public const int NOUNION = 0,
                               UNION = 1,
                               UNION_ALL = 2,
                               INTERSECT = 3,
                               INTERSECT_ALL = 4,
                               EXCEPT_ALL = 5,
                               EXCEPT = 6,
                               UNION_TERM = 7;

        //
        int columnCount;
        private QueryExpression leftQueryExpression;
        private QueryExpression rightQueryExpression;
        public SortAndSlice sortAndSlice;
        private int unionType;
        private bool unionCorresponding;
        private OrderedHashSet unionCorrespondingColumns;
        public int[] unionColumnMap;
        public SqlType[] unionColumnTypes;
        public byte[] unionColumnNullability;
        public bool isFullOrder;

        //
        public EfzList unresolvedExpressions;

        //
        public bool isResolved;

        //
        public int persistenceScope = TableBase.SCOPE_STATEMENT;

        //
        public int columnMode = TableBase.COLUMNS_REFERENCED;

        //
        public ResultMetaData resultMetaData;
        public bool[] accessibleColumns;

        //
        public View view;
        public bool _isUpdatable;
        public bool _isInsertable;
        public bool isCheckable;
        public bool isTopLevel;

        //
        public TableBase resultTable;
        public Index mainIndex;
        public Index fullIndex;
        public Index orderIndex;

        //
        public ParserDML.CompileContext compileContext;

        public QueryExpression(ParserDML.CompileContext compileContext)
        {
            this.compileContext = compileContext;
            sortAndSlice = SortAndSlice.noSort;
        }

        public QueryExpression(ParserDML.CompileContext compileContext,
                               QueryExpression leftQueryExpression)
            : this(compileContext)
        {



            sortAndSlice = SortAndSlice.noSort;
            this.leftQueryExpression = leftQueryExpression;
        }

        public void addUnion(QueryExpression queryExpression, int unionType)
        {

            sortAndSlice = SortAndSlice.noSort;
            this.rightQueryExpression = queryExpression;
            this.unionType = unionType;

            setFullOrder();
        }

        public virtual void addSortAndSlice(SortAndSlice sortAndSlice)
        {
            this.sortAndSlice = sortAndSlice;
            sortAndSlice.sortUnion = true;
        }

        public void setUnionCorresoponding()
        {
            unionCorresponding = true;
        }

        public void setUnionCorrespondingColumns(OrderedHashSet names)
        {
            unionCorrespondingColumns = names;
        }

        public void setFullOrder()
        {

            isFullOrder = true;

            if (leftQueryExpression == null)
            {
                return;
            }

            leftQueryExpression.setFullOrder();
            rightQueryExpression.setFullOrder();
        }

        public void resolve(Session session)
        {

            resolveReferences(session);
            ExpressionColumn.checkColumnsResolved(unresolvedExpressions);
            resolveTypes(session);
        }

        public void resolve(Session session, RangeVariable[] outerRanges,
                            SqlType[] targetTypes)
        {

            resolveReferences(session);

            if (unresolvedExpressions != null)
            {
                for (int i = 0; i < unresolvedExpressions.size(); i++)
                {
                    Expression e = (Expression)unresolvedExpressions.get(i);
                    EfzList list = e.resolveColumnReferences(outerRanges, null);

                    ExpressionColumn.checkColumnsResolved(list);
                }
            }

            unionColumnTypes = targetTypes;

            resolveTypes(session);
        }

        public virtual void resolveReferences(Session session)
        {

            leftQueryExpression.resolveReferences(session);
            rightQueryExpression.resolveReferences(session);
            addUnresolvedExpressions(leftQueryExpression.unresolvedExpressions);
            addUnresolvedExpressions(rightQueryExpression.unresolvedExpressions);

            if (!unionCorresponding)
            {
                columnCount = leftQueryExpression.getColumnCount();

                int rightCount = rightQueryExpression.getColumnCount();

                if (columnCount != rightCount)
                {
                    throw Error.error(ErrorCode.X_42594);
                }

                unionColumnTypes = new SqlType[columnCount];
                unionColumnNullability = new byte[columnCount];
                leftQueryExpression.unionColumnMap =
                    rightQueryExpression.unionColumnMap = new int[columnCount];

                ArrayUtil.fillSequence(leftQueryExpression.unionColumnMap);
                resolveColumnRefernecesInUnionOrderBy();

                return;
            }

            String[] leftNames = leftQueryExpression.getColumnNames();
            String[] rightNames = rightQueryExpression.getColumnNames();

            if (unionCorrespondingColumns == null)
            {
                unionCorrespondingColumns = new OrderedHashSet();

                OrderedIntHashSet leftColumns = new OrderedIntHashSet();
                OrderedIntHashSet rightColumns = new OrderedIntHashSet();

                for (int i = 0; i < leftNames.Length; i++)
                {
                    String name = leftNames[i];
                    int index = ArrayUtil.find(rightNames, name);

                    if (name.Length > 0 && index != -1)
                    {
                        leftColumns.add(i);
                        rightColumns.add(index);
                        unionCorrespondingColumns.add(name);
                    }
                }

                if (unionCorrespondingColumns.isEmpty())
                {
                    throw Error.error(ErrorCode.X_42579);
                }

                leftQueryExpression.unionColumnMap = leftColumns.toArray();
                rightQueryExpression.unionColumnMap = rightColumns.toArray();
            }
            else
            {
                leftQueryExpression.unionColumnMap =
                    new int[unionCorrespondingColumns.size()];
                rightQueryExpression.unionColumnMap =
                    new int[unionCorrespondingColumns.size()];

                for (int i = 0; i < unionCorrespondingColumns.size(); i++)
                {
                    String name = (String)unionCorrespondingColumns.get(i);
                    int index = ArrayUtil.find(leftNames, name);

                    if (index == -1)
                    {
                        throw Error.error(ErrorCode.X_42579);
                    }

                    leftQueryExpression.unionColumnMap[i] = index;
                    index = ArrayUtil.find(rightNames, name);

                    if (index == -1)
                    {
                        throw Error.error(ErrorCode.X_42579);
                    }

                    rightQueryExpression.unionColumnMap[i] = index;
                }
            }

            columnCount = unionCorrespondingColumns.size();
            unionColumnTypes = new SqlType[columnCount];
            unionColumnNullability = new byte[columnCount];

            resolveColumnRefernecesInUnionOrderBy();
        }

        /**
         * Only simple column reference or column position allowed
         */
        void resolveColumnRefernecesInUnionOrderBy()
        {

            int orderCount = sortAndSlice.getOrderLength();

            if (orderCount == 0)
            {
                return;
            }

            String[] unionColumnNames = getColumnNames();

            for (int i = 0; i < orderCount; i++)
            {
                Expression sort = (Expression)sortAndSlice.exprList.get(i);
                Expression e = sort.getLeftNode();

                if (e.getType() == OpTypes.VALUE)
                {
                    if (e.getDataType().typeCode == Types.SQL_INTEGER)
                    {
                        int index = Convert.ToInt32(e.getValue(null));

                        if (0 < index && index <= unionColumnNames.Length)
                        {
                            sort.getLeftNode().queryTableColumnIndex = index - 1;

                            continue;
                        }
                    }
                }
                else if (e.getType() == OpTypes.COLUMN)
                {
                    int index = ArrayUtil.find(unionColumnNames,
                                               e.getColumnName());

                    if (index >= 0)
                    {
                        sort.getLeftNode().queryTableColumnIndex = index;

                        continue;
                    }
                }

                throw Error.error(ErrorCode.X_42576);
            }

            sortAndSlice.prepare(null);
        }

        private void addUnresolvedExpressions(EfzList expressions)
        {

            if (expressions == null)
            {
                return;
            }

            if (unresolvedExpressions == null)
            {
                unresolvedExpressions = new ArrayListIdentity();
            }

            unresolvedExpressions.addAll(expressions);
        }

        public virtual void resolveTypes(Session session)
        {

            if (isResolved)
            {
                return;
            }

            resolveTypesPartOne(session);
            resolveTypesPartTwo(session);

            isResolved = true;
        }

        public virtual void resolveTypesPartOne(Session session)
        {
           
            ArrayUtil.projectRowReverse(leftQueryExpression.unionColumnTypes,
                                        leftQueryExpression.unionColumnMap,
                                        unionColumnTypes);
            ArrayUtil.projectRowReverse(leftQueryExpression.unionColumnNullability,
                                      leftQueryExpression.unionColumnMap,
                                      unionColumnNullability);
            leftQueryExpression.resolveTypesPartOne(session);
            ArrayUtil.projectRow(leftQueryExpression.unionColumnTypes,
                                 leftQueryExpression.unionColumnMap,
                                 unionColumnTypes);
            ArrayUtil.projectRow(leftQueryExpression.unionColumnNullability,
                               leftQueryExpression.unionColumnMap,
                               unionColumnNullability);
                       
            ArrayUtil.projectRowReverse(rightQueryExpression.unionColumnTypes,
                                        rightQueryExpression.unionColumnMap,
                                        unionColumnTypes);
            ArrayUtil.projectRowReverse(rightQueryExpression.unionColumnNullability,
                                       rightQueryExpression.unionColumnMap,
                                       unionColumnNullability);
            rightQueryExpression.resolveTypesPartOne(session);
            ArrayUtil.projectRow(rightQueryExpression.unionColumnTypes,
                                 rightQueryExpression.unionColumnMap,
                                 unionColumnTypes);
            ArrayUtil.projectRow(rightQueryExpression.unionColumnNullability,
                                rightQueryExpression.unionColumnMap,
                                unionColumnNullability);     
     
        }

        public virtual void resolveTypesPartTwo(Session session)
        {
           
            ArrayUtil.projectRowReverse(leftQueryExpression.unionColumnTypes,
                                        leftQueryExpression.unionColumnMap,
                                        unionColumnTypes);
            ArrayUtil.projectRowReverse(leftQueryExpression.unionColumnNullability,
                                        leftQueryExpression.unionColumnMap,
                                        unionColumnNullability);
            leftQueryExpression.resolveTypesPartTwo(session);           

      
            ArrayUtil.projectRowReverse(rightQueryExpression.unionColumnTypes,
                                        rightQueryExpression.unionColumnMap,
                                        unionColumnTypes);
            ArrayUtil.projectRowReverse(rightQueryExpression.unionColumnNullability,
                                        rightQueryExpression.unionColumnMap,
                                        unionColumnNullability);
            rightQueryExpression.resolveTypesPartTwo(session);
      

        //
       
            resultMetaData = leftQueryExpression.getMetaData().getNewMetaData(
                leftQueryExpression.unionColumnMap);

            createTable(session);

           
            if (sortAndSlice.hasOrder())
            {
                QueryExpression queryExpression = this;

                while (true)
                {
                    if (queryExpression.leftQueryExpression == null
                            || queryExpression.unionCorresponding)
                    {
                        sortAndSlice.setIndex(queryExpression.resultTable);

                        break;
                    }

                    queryExpression = queryExpression.leftQueryExpression;
                }
            }
        }

        public Object[] getValues(Session session)
        {

            Result r = getResult(session, 2);
            int size = r.getNavigator().getSize();

            if (size == 0)
            {
                return new Object[r.metaData.getColumnCount()];
            }
            else if (size == 1)
            {
                return r.getSingleRowData();
            }
            else
            {
                throw Error.error(ErrorCode.X_21000);
            }
        }

        public Object[] getSingleRowValues(Session session)
        {

            Result r = getResult(session, 2);
            int size = r.getNavigator().getSize();

            if (size == 0)
            {
                return null;
            }
            else if (size == 1)
            {
                return r.getSingleRowData();
            }
            else
            {
                throw Error.error(ErrorCode.X_21000);
            }
        }

        public Object getValue(Session session)
        {

            Object[] values = getValues(session);

            return values[0];
        }

        public virtual Result getResult(Session session, int maxRows)
        {

            int currentMaxRows = unionType == UNION_ALL ? maxRows
                                                           : int.MaxValue;
            Result first = leftQueryExpression.getResult(session, currentMaxRows);
            RowSetNavigatorData navigator =
                (RowSetNavigatorData)first.getNavigator();
            Result second = rightQueryExpression.getResult(session,
                currentMaxRows);
            RowSetNavigatorData rightNavigator =
                (RowSetNavigatorData)second.getNavigator();

            if (unionCorresponding)
            {
                RowSetNavigatorData rowSet = new RowSetNavigatorData(session,
                    this);

                rowSet.copy(navigator, leftQueryExpression.unionColumnMap);

                navigator = rowSet;

                first.setNavigator(navigator);

                first.metaData = this.getMetaData();
                rowSet = new RowSetNavigatorData(session, this);

                if (unionType != UNION && unionType != UNION_ALL)
                {
                    rowSet.copy(rightNavigator,
                                rightQueryExpression.unionColumnMap);

                    rightNavigator = rowSet;
                }
            }

            switch (unionType)
            {

                case UNION:
                    navigator.union(rightNavigator,
                                    rightQueryExpression.unionColumnMap);
                    break;

                case UNION_ALL:
                    navigator.unionAll(rightNavigator,
                                       rightQueryExpression.unionColumnMap);
                    break;

                case INTERSECT:
                    navigator.intersect(rightNavigator);
                    break;

                case INTERSECT_ALL:
                    navigator.intersectAll(rightNavigator);
                    break;

                case EXCEPT:
                    navigator.except(rightNavigator);
                    break;

                case EXCEPT_ALL:
                    navigator.exceptAll(rightNavigator);
                    break;

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "QueryExpression");
            }

            if (sortAndSlice.hasOrder())
            {
                RowSetNavigatorData nav =
                    (RowSetNavigatorData)first.getNavigator();

                nav.sortUnion(sortAndSlice);
                nav.trim(sortAndSlice.getLimitStart(session),
                         sortAndSlice.getLimitCount(session, maxRows));
            }

            navigator.reset();

            return first;
        }

        public virtual bool isSingleColumn()
        {
            return leftQueryExpression.isSingleColumn();
        }

        public virtual ResultMetaData getMetaData()
        {

            if (resultMetaData != null)
            {
                return resultMetaData;
            }

            return leftQueryExpression.getMetaData();
        }

        public QuerySpecification getMainSelect()
        {

            if (leftQueryExpression == null)
            {
                return (QuerySpecification)this;
            }

            return leftQueryExpression.getMainSelect();
        }

        /** @todo 1.9.0 review */
        public virtual String describe(Session session)
        {
            return leftQueryExpression.describe(session);
        }

        public EfzList getUnresolvedExpressions()
        {
            return unresolvedExpressions;
        }

        public virtual bool areColumnsResolved()
        {
            return unresolvedExpressions == null
                   || unresolvedExpressions.isEmpty();
        }

        public virtual String[] getColumnNames()
        {

            if (unionCorrespondingColumns == null)
            {
                return leftQueryExpression.getColumnNames();
            }

            String[] names = new String[unionCorrespondingColumns.size()];

            unionCorrespondingColumns.toArray(names);

            return names;
        }
        
       
        public virtual SqlType[] getColumnTypes()
        {
            return unionColumnTypes;
        }

        public virtual byte[] getColumnNullability()
        {
            return unionColumnNullability;
        }

        public virtual int getColumnCount()
        {

            if (unionCorrespondingColumns == null)
            {
                int left = leftQueryExpression.getColumnCount();
                int right = rightQueryExpression.getColumnCount();

                if (left != right)
                {
                    throw Error.error(ErrorCode.X_42594);
                }

                return left;
            }

            return unionCorrespondingColumns.size();
        }

        public virtual void collectAllExpressions(EfzList set, OrderedIntHashSet typeSet,
                                          OrderedIntHashSet stopAtTypeSet)
        {

            leftQueryExpression.collectAllExpressions(set, typeSet, stopAtTypeSet);

            if (rightQueryExpression != null)
            {
                rightQueryExpression.collectAllExpressions(set, typeSet,
                        stopAtTypeSet);
            }
        }

        public virtual void collectObjectNames(Set set)
        {

            leftQueryExpression.collectObjectNames(set);

            if (rightQueryExpression != null)
            {
                rightQueryExpression.collectObjectNames(set);
            }
        }

        public HashMappedList getColumns()
        {

            this.getResultTable();

            return ((TableDerived)getResultTable()).columnList;
        }

        /**
         * Used prior to type resolution
         */
        public void setView(View view)
        {
            this.view = view;
        }

        /**
         * Used in views after full type resolution
         */
        public void setTableColumnNames(HashMappedList list)
        {

            if (resultTable != null)
            {
                ((TableDerived)resultTable).columnList = list;

                return;
            }

            leftQueryExpression.setTableColumnNames(list);
        }

        public virtual void createTable(Session session)
        {

            createResultTable(session);

            mainIndex = resultTable.getPrimaryIndex();

            if (sortAndSlice.hasOrder())
            {
                orderIndex = resultTable.createAndAddIndexStructure(null,
                        sortAndSlice.sortOrder, sortAndSlice.sortDescending,
                        sortAndSlice.sortNullsLast, false, false, false);
            }

            int[] fullCols = new int[columnCount];

            ArrayUtil.fillSequence(fullCols);

            fullIndex = resultTable.createAndAddIndexStructure(null, fullCols,
                    null, null, false, false, false);
            resultTable.fullIndex = fullIndex;
        }

        public virtual void createResultTable(Session session)
        {

            QNameManager.QName tableName;
            HashMappedList columnList;
            int tableType;

            tableName = session.database.nameManager.getSubqueryTableName();
            tableType = persistenceScope == TableBase.SCOPE_STATEMENT
                        ? TableBase.SYSTEM_SUBQUERY
                        : TableBase.RESULT_TABLE;
            columnList = leftQueryExpression.getUnionColumns();

            try
            {
                resultTable = new TableDerived(session.database, tableName,
                                               tableType, unionColumnTypes,unionColumnNullability,
                                               columnList, null);
            }
            catch (Exception ) { }
        }

        public void setColumnsDefined()
        {

            columnMode = TableBase.COLUMNS_REFERENCED;

            if (leftQueryExpression != null)
            {
                leftQueryExpression.setColumnsDefined();
            }
        }

        /**
         * Not for views. Only used on root node.
         */
        public virtual void setAsTopLevel()
        {

            if (compileContext.getSequences().Length > 0)
            {
                throw Error.error(ErrorCode.X_42598);
            }

            isTopLevel = true;

            setReturningResultSet();
        }

        /**
         * Sets the scope to SESSION for the QueryExpression object that creates
         * the table
         */
        public virtual void setReturningResultSet()
        {

            if (unionCorresponding)
            {
                persistenceScope = TableBase.SCOPE_SESSION;
                columnMode = TableBase.COLUMNS_UNREFERENCED;

                return;
            }

            if (leftQueryExpression != null)
                 leftQueryExpression.setReturningResultSet();
        }

        private HashMappedList getUnionColumns()
        {

            if (unionCorresponding || leftQueryExpression == null)
            {
                HashMappedList columns = ((TableDerived)resultTable).columnList;
                HashMappedList list = new HashMappedList();

                for (int i = 0; i < unionColumnMap.Length; i++)
                {
                    ColumnSchema column = (ColumnSchema)columns.get(i);

                    list.add(column.getName().name, column);
                }

                return list;
            }

            return leftQueryExpression.getUnionColumns();
        }

        public QNameManager.QName[] getResultColumnNames()
        {

            if (resultTable == null)
            {
                return leftQueryExpression.getResultColumnNames();
            }

            HashMappedList list = ((TableDerived)resultTable).columnList;
            QNameManager.QName[] resultColumnNames = new QNameManager.QName[list.size()];

            for (int i = 0; i < resultColumnNames.Length; i++)
            {
                resultColumnNames[i] = ((ColumnSchema)list.get(i)).getName();
            }

            return resultColumnNames;
        }

        public TableBase getResultTable()
        {

            if (resultTable != null)
            {
                return resultTable;
            }

            if (leftQueryExpression != null)
            {
                return leftQueryExpression.getResultTable();
            }

            return null;
        }

        //
        public virtual Table getBaseTable()
        {
            return null;
        }

        public bool isUpdatable()
        {
            return _isUpdatable;
        }

        public bool isInsertable()
        {
            return _isInsertable;
        }

        public virtual int[] getBaseTableColumnMap()
        {
            return null;
        }

        public virtual Expression getCheckCondition()
        {
            return null;
        }

        public virtual bool hasReference(RangeVariable range)
        {

            if (leftQueryExpression.hasReference(range))
            {
                return true;
            }

            if (rightQueryExpression.hasReference(range))
            {
                return true;
            }

            return false;
        }

        public virtual void getBaseTableNames(OrderedHashSet set)
        {
            leftQueryExpression.getBaseTableNames(set);
            rightQueryExpression.getBaseTableNames(set);
        }
    }
}
